Context 都這麼大了,為什麼還需要 RAG?
這篇會用《寶可夢十八氣》當知識庫,快速走過 RAG 從 Chunking、Embedding 到 Retrieval 的基本流程。
LLM 最大的問題之一,是它其實「不知道自己不知道什麼」。它只是根據使用者的輸入,預測下一個最可能出現的 token,所以本質上並不真正理解自己在說什麼,也因此可能出現憑空捏造答案的情況,也就是所謂的 Hallucination。
而 RAG 做的事情,就是先找出 LLM 回答問題時可能需要的資訊,再把這些資訊一併放進 Context,讓模型在回答時有額外的依據,而不是只靠模型本身既有的知識去猜。
但 RAG 也不是萬能的,就最普通的 RAG 來說,雖然能找到直接相關的資訊,但有可能會漏掉整個資料集的 global questions,這也是為什麼會有像 graphRAG 等技術出現,但同樣的問題,現在 AI 模型的上下文窗口動輒百萬 tokens,之後也只會越來越大,全部塞進去不行嗎?幹嘛還要花時間做 RAG 再去找相關資訊?全部塞進去就不需要煩惱什麼關係鏈的遺失啦?
答案是 RAG 還是有存在的必要,一來是能不能塞進去完全取決於文件數量,二來是就算全部塞進去,模型的注意力反而可能因此被分散,所以「模型裝得下多少資料」和「模型能不能有效利用這些資料」根本是兩回事,所以真正的問題是那些資訊應該進入 Context?
對於 Context Engineering 來說,RAG 只是其中一種手段,更新的做法是讓 LLM 幫我們管理知識的 LLM-based Knowledge Management。像是 Karpathy 就提出 llm-wiki 的概念,直接請 LLM agent 維護一個知識 Wiki。但就現階段而言,這表示我們必須相信 LLM 每一次都能正確判斷「我要找什麼、去哪裡找,以及什麼資訊值得保留」,所以可能還不太適合用在生產環境中,而 RAG 仍然有其不可替代的價值。
《寶可夢十八氣》是我長期觀察寶可夢屬性與人的狀態後,慢慢整理出來的一套方法。
我發現十八種屬性,不只可以拿來對戰,也很適合解釋人的情緒、行動力、界線與人生狀態。
既然有幸悟到這套東西,我也不打算藏私。
所以把它完整整理下來,傳給所有有緣的訓練師。
幾種常見的 Chunking 策略:
用固定的文字長度 (或 token 長度) 來切,最簡單,但也最沒有語意結構,拉完了。
在 Fixed-size Chunking 讓每個 Chunk 之間 overlap,減少上下文直接從中間切斷的問題,但一樣是拉完了。
優先按照文件本身比較自然的邊界切割,ex: \n\n,如果還是太大,再往更細的單位切,ex: \n,舉例來說 LangChain 的 RecursiveCharacterTextSplitter:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=[ "\n\n", "\n", "。", " ", "" ]
)
chunks = splitter.split_text(text)
直接利用文件本身的結構,非常適合用在 markdown 這種有固定結構的文件,其他還有像 html, pdf, 或甚至程式碼,都有一套結構可以利用,這些結構本身就帶有一種主題分類的概念。
觀察 寶可夢十八氣.md 的文章結構,發現每個主要段落間都有用 --- 來分隔,真是太貼心了。
with open("寶可夢十八氣.md", encoding="utf-8") as f:
raw_text = f.read()
chunks = raw_text.split("---")
只有文件格式怎麼夠,我要用語意分的乾乾淨淨啊,我們可以計算每個句子 (段落) 的 Embedding 間的 Similarity 的變化,如果 Similarity 突然變低那大概就是一個斷點,或者我們也可以讓 LLM 來替我們切 Chunks。
Embedding 是把文字轉成可以搜尋的語意座標 (高維度空間中的一個點),這些 Embedding Models 在經過大量的資料訓練後,輸出的向量竟然能神奇的反映文字的語意,而語意相似的文字,在 Vector Space 裡應該彼此靠近。
不論輸入的文字多長,同一個模型輸出的向量長度都相同,但不同 Embedding 模型的向量長度可能不同,所代表的意義也是完全不同的。
embedding_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
embeddings = embedding_model.encode(chunks)
dimension = len(embeddings[0])
print(f"Length: {dimension}")
print(embeddings[0])
輸出是:
Length: 384
[ 1.29806727e-01 2.08567604e-01 -8.23545754e-02 1.55131847e-01 ...]
文件轉成 Vector 後需要找的地方存起來,這些 Vector DB 通常都經過特殊設計,能使用 Approximate Nearest Neighbor(ANN) Index 來快速找到相似的 Vector,能省下一筆一筆算 Cosine Similarity 的時間。另外除了存 Vector 外,也會存原始文字和其他的 metadata,在搜尋時可以同時用 Vector Similarity + Metadata Filtering 來大大地加速並縮小範圍。
但這邊為了方便用的是 Milvus Lite,是沒有 ANN 的:
from pymilvus import MilvusClient
milvus_client = MilvusClient("rag.db")
milvus_client.create_collection(
collection_name="documents",
dimension=dimension
)
def to_doc(id, vector, text):
return {
"id": id,
"vector": vector,
"text": text
}
documents = [to_doc(i+1, embedding, chunk) for i, (embedding, chunk) in enumerate(zip(embeddings, chunks))]
milvus_client.insert(
collection_name="documents",
data=documents
)
資料存進 milvus 後,下次使用前要先 load_collection 後才能用:
milvus_client.load_collection(
collection_name="documents"
)
從 milvus 中搜索相似資料:
def RAG(text, top_k=3):
results = milvus_client.search(
collection_name="documents",
data=[embedding_model.encode(text)],
limit=top_k,
output_fields=["text"]
)
return [result["entity"]["text"] for result in results[0]]
系統提示詞:
你是一位精通《寶可夢十八氣》的命理分析師。
請優先根據提供的 RAG 資料,用《寶可夢十八氣》分析使用者的狀況,避免寫成一般心理諮詢。
分析時:
* 判斷最相關的氣,以及較像氣旺、氣衰、氣滯或氣洩。
* 資料足夠時,再分析主位、佐位、調候位。
* 自然加入 2~3 隻寶可夢作為有趣比喻,但不要捏造知識庫中不存在的規則。
* 如果資料不足,仍根據現有資訊做合理推測,並清楚標示哪些是推測。
* 最後簡短說明還需要哪些資訊,才能進一步分析。
回答使用繁體中文,一本正經但可以帶少量幽默。
請保持精簡,控制在 300~500 字左右,避免重複解釋與過長的條列。
實際跑看看:
question = """
最近每天上班都覺得很累,工作內容也越做越煩,常常一想到明天還要上班就很抗拒。
我其實已經很想離職了,但又會擔心離職之後找不到更好的工作,所以一直拖著沒有行動。
我該怎麼做?
"""
client = OpenAI(
api_key=API_KEY,
base_url=BASE_URL
)
rags = RAG(question)
rag_prompt = "\n\n".join(rags)
content = f"問題:\n{question}\n\n請參考以下RAG 檢索到的資料回答問題,不要憑記憶或上網搜尋:\n\n{rag_prompt}"
response = client.responses.create(
model=MODEL,
input=content,
instructions=SYSTEM_PROMPT
)
print(response.output_text)
輸出結果:
依《寶可夢十八氣》,你目前較像**草衰伴隨火滯,並可能有長期氣洩**;這是依現有描述的推測,不是醫療診斷。
**草衰**:每天疲累、工作越做越煩,像妙蛙種子持續被要求出力,卻少有滋養。若也感到學不到東西、看不見成長,便更符合草衰。
**火滯**:你已有離職念頭,卻被「找不到更好工作」卡住,比起沒志氣,更像小火龍有火卻難以發動。資料提醒須兼察其他氣,但目前不足以細判,也不宜硬定主位、佐位、調候位。
眼下宜**先養草,再讓火有小出口**:
- 暫不逼自己立即辭職,也別靠提神或責罵自己「不夠努力」繼續硬撐。
- 先減少可避免的加班,保留休息,辨認最耗損你的究竟是工作量、人際,還是缺乏成長。
- 把離職拆成小步:盤點存款與必要支出、更新履歷、少量投遞,用實際職缺與面試回饋降低未知,不必一下變成衝刺的風速狗。
要進一步辨氣,還需知道:疲累持續多久、休假能否恢復、睡眠如何,以及最早讓你開始抗拒工作的事件。關鍵不是催你走,而是找出最先失衡的一環。
我的 Context 已經滿了,請不要再塞工作進來。